package com.dtflys.easyel.utils;


import com.dtflys.easyel.exception.EasyElAmbiguousMethodsException;
import com.dtflys.easyel.exception.EasyElConstructorException;
import com.dtflys.easyel.runtime.EasyElObject;
import com.dtflys.easyel.runtime.MetaClass;
import com.dtflys.easyel.runtime.MetaClassFactory;
import com.dtflys.easyel.runtime.MethodParameterTypes;
import com.dtflys.easyel.runtime.wrapper.BigDecimalWrapper;
import com.dtflys.easyel.runtime.wrapper.BigIntegerWrapper;
import com.dtflys.easyel.runtime.wrapper.BooleanWrapper;
import com.dtflys.easyel.runtime.wrapper.DoubleWrapper;
import com.dtflys.easyel.runtime.wrapper.FloatWrapper;
import com.dtflys.easyel.runtime.wrapper.IntWrapper;
import com.dtflys.easyel.runtime.wrapper.JavaBeanWrapper;
import com.dtflys.easyel.runtime.wrapper.LongWrapper;
import com.dtflys.easyel.runtime.wrapper.MapWrapper;
import com.dtflys.easyel.runtime.wrapper.ShortWrapper;
import com.dtflys.easyel.runtime.wrapper.StringWrapper;
import com.dtflys.easyel.runtime.wrapper.Wrapper;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @author gongjun[jun.gong@thebeastshop.com]
 * @since v1.0.0
 */
public class MetaHelper {

    public static EasyElObject wrap(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof EasyElObject) {
            return (EasyElObject) obj;
        }
        if (obj instanceof CharSequence) {
            return new StringWrapper((CharSequence) obj);
        }
        if (obj instanceof Boolean) {
            return new BooleanWrapper((Boolean) obj);
        }
        if (obj instanceof Short) {
            return new ShortWrapper((Short) obj);
        }
        if (obj instanceof Integer) {
            return new IntWrapper((Integer) obj);
        }
        if (obj instanceof Long) {
            return new LongWrapper((Long) obj);
        }
        if (obj instanceof BigInteger) {
            return new BigIntegerWrapper((BigInteger) obj);
        }
        if (obj instanceof Float) {
            return new FloatWrapper((Float) obj);
        }
        if (obj instanceof Double) {
            return new DoubleWrapper((Double) obj);
        }
        if (obj instanceof BigDecimal) {
            return new BigDecimalWrapper((BigDecimal) obj);
        }
        if (obj instanceof Map) {
            return new MapWrapper((Map) obj);
        }
        Wrapper wrapper = new JavaBeanWrapper(obj, obj.getClass());
        return wrapper;
    }

    public static Object unwrap(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof Wrapper) {
            Object ret = ((Wrapper) obj).getOriginalObject();
            if (ret instanceof Collection) {
                return unwrap((Collection) ret);
            } else if (ret instanceof Map) {
                return unwrap((Map) ret);
            }
            return ret;
        }
        return obj;
    }


    public static Object[] wrapArray(Object args) {
        int len = Array.getLength(args);
        Object[] rets = new EasyElObject[len];
        for (int i = 0; i < len; i++) {
            Object elem = Array.get(args, i);
            if (elem.getClass().isArray()) {
                elem = wrapArray(elem);
            } else {
                elem = wrap(elem);
            }
            rets[i] = elem;
        }
        return rets;
    }


    public static Object[] unwrap(Object[] args) {
        int len = args.length;
        Object[] rets = new Object[len];
        for (int i = 0; i < len; i++) {
            Object arg = args[i];
            Object unwrapped = unwrap(arg);
            rets[i] = unwrapped;
        }
        return rets;
    }

    public static Collection unwrap(Collection list) {
        if (list instanceof EasyElObject) {
            return list;
        }

        Class listType = list.getClass();
        Collection newList;
        try {
            newList = (Collection) listType.newInstance();
        } catch (InstantiationException e) {
            newList = new ArrayList();
        } catch (IllegalAccessException e) {
            newList = new ArrayList();
        }
        for (Object elem : list) {
            Object obj = unwrap(elem);
            newList.add(obj);
        }
        return newList;
    }


    public static Map unwrap(Map map) {
        if (map instanceof EasyElObject) {
            return map;
        }

        Class mapType = map.getClass();
        Map newMap;

        try {
            newMap = (Map) mapType.newInstance();
        } catch (InstantiationException e) {
            newMap = new HashMap(map.size());
        } catch (IllegalAccessException e) {
            newMap = new HashMap(map.size());
        }

        for (Object key : map.keySet()) {
            Object value = map.get(key);
            Object newKey = unwrap(key);
            Object newValue = unwrap(value);
            newMap.put(newKey, newValue);
        }

        return newMap;
    }

    public static Class autoboxType(Class primitiveType) {
        if (primitiveType == byte.class) {
            return Byte.class;
        }
        if (primitiveType == char.class) {
            return Character.class;
        }
        if (primitiveType == short.class) {
            return Short.class;
        }
        if (primitiveType == int.class) {
            return Integer.class;
        }
        if (primitiveType == long.class) {
            return Long.class;
        }
        if (primitiveType == float.class) {
            return Float.class;
        }
        if (primitiveType == double.class) {
            return Double.class;
        }
        return primitiveType;
    }


    public static Object[] castArguments(MethodParameterTypes parameterTypes, Object[] arguments) {
        int len = arguments.length;
        Class[] targetTypes = parameterTypes.getTypes();
        int nonVarLength = parameterTypes.getNonVarLength();
        int castedLen = parameterTypes.isVarArgs() ? nonVarLength + 1 : nonVarLength;
        Object[] casted = new Object[castedLen];
        for (int i = 0; i < nonVarLength; i++) {
            Class type = autoboxType(targetTypes[i]);
            Object obj = arguments[i];
            Object castedObj = cast(type, obj);
            casted[i] = castedObj;
        }
        // cast var args
        if (parameterTypes.isVarArgs()) {
            Class varArgType = parameterTypes.getVarArgType();
            Class boxedVarArgType = autoboxType(varArgType);
            Object castedVarArgs = Array.newInstance(varArgType, len - nonVarLength);
            casted[nonVarLength] = castedVarArgs;
            for (int i = nonVarLength, j = 0; i < len; i++, j++) {
                Object obj = arguments[i];
                Object castedObj = cast(boxedVarArgType, obj);
                Array.set(castedVarArgs, j, castedObj);
            }
        }
        return casted;
    }

    public static <T> T cast(Class<T> targetType, Object obj) {
        if (obj == null) {
            return null;
        }
        Class objClass = obj.getClass();
        if (targetType == obj) {
            return (T) obj;
        }
        if (Number.class.isAssignableFrom(targetType) &&
                obj instanceof Number) {
            return (T) NumberHelper.castNumber(targetType, (Number) obj);
        }
        if (targetType.isAssignableFrom(objClass)) {
            return targetType.cast(obj);
        }
        if (String.class.isAssignableFrom(targetType) && obj instanceof CharSequence) {
            return (T) String.valueOf(obj);
        }
        throw new ClassCastException(obj.toString() + " can not be cast to " + targetType.getName());
    }

    public static int calculateTypeDistance(Class paramType, Class argType) {
        if (paramType == argType) {
            return 0;
        }
        if (paramType.isInterface()) {
            int d = calculateInterfaceDistance(paramType, argType);
            if (d > -1) {
                return d;
            }
        }
        int distance = 0;
        Class superClass = argType;
        while (superClass != null &&
                superClass != paramType) {
            superClass = superClass.getSuperclass();
            distance += 3;
        }
        return distance;
    }


    private static int calculateInterfaceDistance(Class interfaceClass, Class targetType) {
        if (interfaceClass == targetType) {
            return 0;
        }
        Class[] interfaces = targetType.getInterfaces();
        int ret = -1;
        for (Class targetInterface : interfaces) {
            int d = calculateInterfaceDistance(interfaceClass, targetInterface);
            if (d != -1) {
                d += 1;
            }
            if (ret < d) {
                ret = d;
            }
        }
        Class superClass = targetType.getSuperclass();
        if (superClass != null) {
            int d = calculateInterfaceDistance(interfaceClass, superClass);
            if (d != -1) {
                d += 1;
            }
            if (ret < d) {
                ret = d;
            }
        }
        return ret;
    }

    public static String toArgumentTypesString(Object[] arguments) {
        StringBuilder builder = new StringBuilder();
        builder.append('(');
        for (int i = 0; i < arguments.length; i++) {
            Object arg = arguments[i];
            arg = unwrap(arg);
            Class argType = arg.getClass();
            builder.append(argType.getSimpleName());
            if (i < arguments.length - 1) {
                builder.append(", ");
            }
        }
        builder.append(')');
        return builder.toString();
    }


    public static Object newInstance(Class clazz, Object[] arguments)
            throws InvocationTargetException, EasyElAmbiguousMethodsException, EasyElConstructorException, InstantiationException, IllegalAccessException {
        MetaClass metaClass = MetaClassFactory.getMetaClass(clazz);
        Object ret = metaClass.newInstance(arguments);
        return wrap(ret);
    }
}
